import pandas as pd
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
import logging
import time
from colorama import init, Fore, Style
from jinja2 import Template
import matplotlib.pyplot as plt
import base64
from io import BytesIO
import os
from datetime import datetime
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains


class TestResult:
    def __init__(self, case_id, test_point, step_number, step_desc, status, error_msg=None, screenshot=None):
        self.case_id = case_id
        self.test_point = test_point
        self.step_number = step_number
        self.step_desc = step_desc
        self.status = status
        self.error_msg = error_msg
        self.screenshot = screenshot
        self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")


class AndroidUITest:
    def __init__(self):
        self.driver = None
        self.setup_logging()
        init()
        self.test_results = []

        # 添加matplotlib配置
        plt.rcParams['font.sans-serif'] = ['SimHei']
        plt.rcParams['axes.unicode_minus'] = False
        logging.getLogger('matplotlib').setLevel(logging.ERROR)

    def setup_logging(self):
        """设置日志记录"""
        # 确保logs目录存在
        log_dir = '../logs'
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        # 生成日志文件名
        log_file = os.path.join(log_dir, '../test_execution.log')

        # 设置日志格式
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

        # 创建文件处理器，指定编码为utf-8
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setFormatter(formatter)

        # 创建控制台处理器
        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)

        # 配置根日志记录器
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)

        # 清除现有的处理器
        logger.handlers.clear()

        # 添加处理器
        logger.addHandler(file_handler)
        logger.addHandler(console_handler)

    def setup_driver(self):
        caps = {
            "platformName": "Android",
            "platformVersion": "9",
            "appPackage": "com.seven.qaml.burstserven",
            "appActivity": "com.seven.qaml.burstserven.ui.LoginActivity",
            "deviceName": "2023110100001087",
            "automationName": "Uiautomator2",
            "skipServerInstallation" : True,
            "skipDeviceInitialization" : True,
            "noReset": True
        }
        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', caps)

    def read_test_cases(self, excel_path):
        try:
            df = pd.read_excel(excel_path)
            return df
        except Exception as e:
            logging.error(f"读取Excel文件失败: {str(e)}")
            return None

    def locate_element(self, locator_type, locator_value):
        """根据定位方式查找元素"""
        try:
            if locator_type == "id":
                element = self.driver.find_element(MobileBy.ID, locator_value)
            elif locator_type == "xpath":
                element = self.driver.find_element(MobileBy.XPATH, locator_value)
            elif locator_type == "accessibility_id":
                element = self.driver.find_element(MobileBy.ACCESSIBILITY_ID, locator_value)
            return element
        except Exception as e:
            logging.error(f"元素定位失败: {str(e)}")
            return None

    def swipe_screen(self, start_x, start_y, end_x, end_y, duration=None):
        """执行滑动操作"""
        try:
            screen_size = self.driver.get_window_size()
            if isinstance(start_x, float):
                start_x = screen_size['width'] * start_x
            if isinstance(start_y, float):
                start_y = screen_size['height'] * start_y
            if isinstance(end_x, float):
                end_x = screen_size['width'] * end_x
            if isinstance(end_y, float):
                end_y = screen_size['height'] * end_y

            self.driver.swipe(start_x, start_y, end_x, end_y, duration)
            return True
        except Exception as e:
            logging.error(f"滑动操作失败: {str(e)}")
            return False

    def get_element_text(self, locator_type, locator_value):
        """获取元素文本内容"""
        element = self.locate_element(locator_type, locator_value)
        if element:
            return element.text
        return None

    def is_element_exist(self, locator_type, locator_value, timeout=10):
        """验证元素是否存在"""
        try:
            if locator_type == "id":
                self.driver.find_element(MobileBy.ID, locator_value)
            elif locator_type == "xpath":
                self.driver.find_element(MobileBy.XPATH, locator_value)
            elif locator_type == "accessibility_id":
                self.driver.find_element(MobileBy.ACCESSIBILITY_ID, locator_value)
            return True
        except:
            return False

    def take_screenshot(self, name=None):
        """截图功能"""
        try:
            if name is None:
                name = time.strftime("%Y%m%d_%H%M%S")
            file_path = f"screenshots/{name}.png"
            self.driver.get_screenshot_as_file(file_path)
            logging.info(f"截图保存成功: {file_path}")
            return True
        except Exception as e:
            logging.error(f"截图失败: {str(e)}")
            return False

    def is_toast_exist(self, text, timeout=10):
        """验证toast是否存在"""
        try:
            toast_loc = f".//*[contains(@text,'{text}')]"
            toast_element = self.driver.find_element(MobileBy.XPATH, toast_loc)
            return True
        except:
            return False

    def verify_toast_text(self, expected_text, timeout=10):
        """验证toast文本内容"""
        try:
            toast_loc = f".//*[contains(@text,'{expected_text}')]"
            toast_element = self.driver.find_element(MobileBy.XPATH, toast_loc)
            actual_text = toast_element.text
            return actual_text == expected_text
        except:
            return False

    def print_step_result(self, step_number, test_step, success, error_msg=None):
        """打印带颜色的步骤执行结果"""
        if success:
            print(f"{Fore.GREEN}步骤 {step_number} [{test_step}] - 执行成功{Style.RESET_ALL}")
        else:
            print(f"{Fore.RED}步骤 {step_number} [{test_step}] - 执行失败{Style.RESET_ALL}")
            if error_msg:
                print(f"{Fore.RED}错误信息: {error_msg}{Style.RESET_ALL}")

    def generate_pie_chart(self, success_count, failed_count):
        """生成测试结果饼图"""
        plt.figure(figsize=(8, 8))

        # 设置中文字体
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
        plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

        labels = ['成功', '失败']
        sizes = [success_count, failed_count]
        colors = ['#28a745', '#dc3545']
        plt.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
        plt.axis('equal')

        # 将图表转换为base64字符串，并关闭志输出
        import logging
        logging.getLogger('matplotlib').setLevel(logging.ERROR)  # 禁用matplotlib警告

        buffer = BytesIO()
        plt.savefig(buffer, format='png', bbox_inches='tight')
        buffer.seek(0)
        image_png = buffer.getvalue()
        buffer.close()
        plt.close()

        return base64.b64encode(image_png).decode()

    def generate_html_report(self):
        """生成HTML测试报告"""
        try:
            # 1. 统计数据
            success_count = len([r for r in self.test_results if r.status == 'success'])
            failed_count = len([r for r in self.test_results if r.status == 'failure'])
            total_cases = len(set(r.case_id for r in self.test_results))  # 使用集合去重
            pass_rate = (success_count / len(self.test_results) * 100) if self.test_results else 0

            # 2. 按测试用例ID分组
            results_by_case = {}
            for result in self.test_results:
                if result.case_id not in results_by_case:
                    results_by_case[result.case_id] = []
                results_by_case[result.case_id].append({
                    'step_number': result.step_number,
                    'step_desc': result.step_desc,
                    'status': result.status,
                    'error_msg': result.error_msg,
                    'screenshot': result.screenshot,
                    'timestamp': result.timestamp,
                    'test_point': result.test_point
                })

            # 3. 准备模板数据
            template_data = {
                'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'total_cases': total_cases,
                'success_count': success_count,
                'failed_count': failed_count,
                'pass_rate': round(pass_rate, 2),
                'cases': []
            }

            # 4. 整理测试用例数据
            for case_id, steps in results_by_case.items():
                case_data = {
                    'id': case_id,
                    'test_point': steps[0]['test_point'],
                    'status': 'success' if all(s['status'] == 'success' for s in steps) else 'failure',
                    'steps': steps
                }
                template_data['cases'].append(case_data)

            # 5. 模板内容
            html_template = """
            <!DOCTYPE html>
            <html>
            <head>
                <title>APP UI自动化测试报告</title>
                <meta charset="utf-8">
                <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
                <style>
                    body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
                    .container { max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
                    .header { text-align: center; padding: 20px; background: linear-gradient(135deg, #6e8efb, #a777e3); color: white; border-radius: 8px; margin-bottom: 30px; }
                    .statistics-section { display: flex; justify-content: space-around; margin: 20px 0; flex-wrap: wrap; }
                    .summary { text-align: center; padding: 15px; background-color: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); min-width: 200px; margin: 10px; }
                    .chart-container { width: 400px; margin: 20px auto; }
                    .test-case { border: 1px solid #ddd; margin-bottom: 15px; padding: 15px; border-radius: 8px; }
                    .success { border-left: 5px solid #28a745; }
                    .failure { border-left: 5px solid #dc3545; }
                    .status-badge { display: inline-block; padding: 5px 10px; border-radius: 15px; color: white; font-weight: bold; }
                    .status-success { background-color: #28a745; }
                    .status-failure { background-color: #dc3545; }
                    .error-message { background-color: #fff3f3; padding: 10px; border-radius: 5px; margin-top: 10px; color: #dc3545; font-family: monospace; }
                    .timestamp { color: #666; font-size: 0.9em; }
                    .screenshot { max-width: 100%; margin-top: 10px; border-radius: 4px; }
                    
                    /* 添加新的样式 */
                    .screenshot-thumbnail {
                        max-width: 200px;
                        max-height: 200px;
                        cursor: pointer;
                        border: 1px solid #ddd;
                        border-radius: 4px;
                        transition: transform 0.2s;
                    }
                    
                    .screenshot-thumbnail:hover {
                        transform: scale(1.05);
                    }
                    
                    /* 模态框样式 */
                    .modal {
                        display: none;
                        position: fixed;
                        z-index: 1000;
                        top: 0;
                        left: 0;
                        width: 100%;
                        height: 100%;
                        background-color: rgba(0,0,0,0.9);
                    }
                    
                    .modal-content {
                        margin: auto;
                        display: block;
                        max-width: 90%;
                        max-height: 90%;
                        margin-top: 2%;
                    }
                    
                    .close {
                        position: absolute;
                        right: 35px;
                        top: 15px;
                        color: #f1f1f1;
                        font-size: 40px;
                        font-weight: bold;
                        cursor: pointer;
                    }
                    
                    .close:hover {
                        color: #bbb;
                    }
                    
                    /* 图片加载动画 */
                    @keyframes fadeIn {
                        from {opacity: 0}
                        to {opacity: 1}
                    }
                    
                    .fade-in {
                        animation: fadeIn 0.5s;
                    }
                </style>
            </head>
            <body>
                <!-- 添加模态框 -->
                <div id="imageModal" class="modal">
                    <span class="close">&times;</span>
                    <img class="modal-content" id="modalImage">
                </div>
                
                <div class="container">
                    <div class="header">
                        <h1>APP UI自动化测试报告</h1>
                        <p>测试执行时间: {{timestamp}}</p>
                    </div>

                    <div class="statistics-section">
                        <div class="summary">
                            <h3>总用例数</h3>
                            <p>{{total_cases}}</p>
                        </div>
                        <div class="summary">
                            <h3>通过用例</h3>
                            <p style="color: #28a745">{{success_count}}</p>
                        </div>
                        <div class="summary">
                            <h3>失败用例</h3>
                            <p style="color: #dc3545">{{failed_count}}</p>
                        </div>
                        <div class="summary">
                            <h3>通过率</h3>
                            <p>{{pass_rate}}%</p>
                        </div>
                    </div>

                    <div class="chart-container">
                        <canvas id="resultChart"></canvas>
                    </div>

                    <script>
                        var ctx = document.getElementById('resultChart').getContext('2d');
                        new Chart(ctx, {
                            type: 'pie',
                            data: {
                                labels: ['通过用例', '失败用例'],
                                datasets: [{
                                    data: [{{success_count}}, {{failed_count}}],
                                    backgroundColor: ['#28a745', '#dc3545'],
                                    borderColor: ['#ffffff', '#ffffff'],
                                    borderWidth: 2
                                }]
                            },
                            options: {
                                responsive: true,
                                plugins: {
                                    legend: { position: 'bottom' },
                                    title: { display: true, text: '测试用例执行结果统计' }
                                }
                            }
                        });
                    </script>

                    {% for case in cases %}
                    <div class="test-case {{case.status}}">
                        <h3>测试用例: {{case.id}}</h3>
                        <p>测试点: {{case.test_point}}</p>
                        
                        {% for step in case.steps %}
                        <div class="test-step">
                            <h4>步骤 {{step.step_number}}: {{step.step_desc}}</h4>
                            <span class="status-badge status-{{step.status}}">
                                {% if step.status == 'success' %}✓ 成功{% else %}✗ 失败{% endif %}
                            </span>
                            <span class="timestamp">{{step.timestamp}}</span>
                            
                            {% if step.error_msg %}
                            <div class="error-message">
                                <p>错误信息: {{step.error_msg}}</p>
                            </div>
                            {% endif %}
                            
                            {% if step.screenshot %}
                            <div class="screenshot-container">
                                <img class="screenshot-thumbnail" 
                                     src="data:image/png;base64,{{step.screenshot}}" 
                                     alt="测试截图"
                                     onclick="showImage(this)">
                            </div>
                            {% endif %}
                        </div>
                        {% endfor %}
                    </div>
                    {% endfor %}
                </div>
                
                <script>
                    // 模态框功能
                    var modal = document.getElementById("imageModal");
                    var modalImg = document.getElementById("modalImage");
                    var span = document.getElementsByClassName("close")[0];
                    
                    function showImage(img) {
                        modal.style.display = "block";
                        modalImg.src = img.src;
                        modalImg.classList.add('fade-in');
                    }
                    
                    span.onclick = function() {
                        modal.style.display = "none";
                    }
                    
                    modal.onclick = function(event) {
                        if (event.target == modal) {
                            modal.style.display = "none";
                        }
                    }
                    
                    // 按ESC键关闭图片
                    document.addEventListener('keydown', function(event) {
                        if (event.key === "Escape") {
                            modal.style.display = "none";
                        }
                    });
                    
                    // ... 原有的Chart.js代码保持不变 ...
                </script>
            </body>
            </html>
            """

            # 6. 渲染模板
            template = Template(html_template)
            html_content = template.render(**template_data)

            # 7. 保存报告
            report_dir = "../test_reports"
            if not os.path.exists(report_dir):
                os.makedirs(report_dir)

            report_path = os.path.join(report_dir, f"test_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html")
            with open(report_path, 'w', encoding='utf-8') as f:
                f.write(html_content)

            return report_path

        except Exception as e:
            logging.error(f"生成报告失败: {str(e)}")
            raise

    def send_keys(self, key_code):
        """
        发送安卓物理按键
        :param key_code: 按键键值，例如：
            3: HOME键
            4: BACK返回键
            24: 音量+
            25: 音量-
        """
        try:
            key_code = int(key_code)
            self.driver.press_keycode(key_code)
            logging.info(f"按键操作成功: keycode={key_code}")
            return True
        except Exception as e:
            logging.error(f"按键操作失败: {str(e)}")
            return False,e

    def execute_step(self, step_data):
        """执行测试步骤"""
        try:
            test_case_id = step_data['测试用例编号']
            test_point = step_data['测试点']
            step_number = step_data['步骤编号']
            test_step = step_data['测试步骤']
            is_executed = step_data['是否执行']
            keyword = step_data['测试步骤关键字']
            locator_type = step_data['定位方式']
            locator_value = step_data['定位地址']
            param_value = step_data['参数值']
            expected_result = step_data['预期结果']

            if is_executed != 'y':
                logging.info(f"步骤 {step_number} 被跳过")
                print(f"{Fore.YELLOW}步骤 {step_number} [{test_step}] - 已跳过{Style.RESET_ALL}")
                return True

            logging.info(f"执行步骤 {step_number}: {test_step}")

            # 执行关键字对应的操作
            if keyword == "点击":
                element = self.locate_element(locator_type, locator_value)
                if element:
                    element.click()
                    self.print_step_result(step_number, test_step, True)
                else:
                    raise Exception("元素未找到")
            elif keyword == "输入":
                element = self.locate_element(locator_type, locator_value)
                if element:
                    element.send_keys(param_value)
                    self.print_step_result(step_number, test_step, True)
                else:
                    raise Exception("元素未找到")
            elif keyword == "按键":
                self.send_keys(param_value)
                self.print_step_result(step_number, test_step, True)
            elif keyword == "等待":
                time.sleep(float(param_value))
            elif keyword == "滑动":
                # 参数值格式：start_x,start_y,end_x,end_y,duration
                swipe_params = param_value.split(',')
                self.swipe_screen(float(swipe_params[0]), float(swipe_params[1]),
                                  float(swipe_params[2]), float(swipe_params[3]),
                                  int(swipe_params[4]) if len(swipe_params) > 4 else None)
            elif keyword == "获取文本":
                text = self.get_element_text(locator_type, locator_value)
                if text and expected_result:
                    assert text == expected_result, f"文本不匹配，期望：{expected_result}，实际：{text}"
            elif keyword == "验证元素存在":
                assert self.is_element_exist(locator_type, locator_value), "元素不存在"
            elif keyword == "截图":
                self.take_screenshot(param_value)
            elif keyword == "is_toast_exist":
                assert self.is_toast_exist(expected_result), f"Toast消息 '{expected_result}' 未找到"
            elif keyword == "verify_toast_text":
                assert self.verify_toast_text(expected_result), f"Toast内容不匹配：{expected_result}"

            # 执行成功后截图
            screenshot = self.take_screenshot_as_base64()
            
            # 记录测试结果
            self.test_results.append(TestResult(
                case_id=test_case_id,
                test_point=test_point,
                step_number=step_number,
                step_desc=test_step,
                status='success',
                screenshot=screenshot
            ))

            return True

        except Exception as e:
            error_msg = str(e)
            logging.error(f"步骤执行失败: {error_msg}")
            self.print_step_result(step_number, test_step, False, error_msg)

            # 执行失败后截图
            screenshot = self.take_screenshot_as_base64()
            
            # 记录失败的测试结果
            self.test_results.append(TestResult(
                case_id=test_case_id,
                test_point=test_point,
                step_number=step_number,
                step_desc=test_step,
                status='failure',
                error_msg=error_msg,
                screenshot=screenshot
            ))

            return False

    def take_screenshot_as_base64(self):
        """获取base64格式的截图"""
        try:
            # 确保screenshots目录存在
            if not os.path.exists("../screenshots"):
                os.makedirs("../screenshots")
            
            # 获取截图
            screenshot_base64 = self.driver.get_screenshot_as_base64()
            logging.info("截图成功")
            return screenshot_base64
        except Exception as e:
            logging.error(f"截图失败: {str(e)}")
            return None

    def run_test_suite(self, excel_path):
        """运行测试套件"""
        try:
            test_cases = self.read_test_cases(excel_path)
            if test_cases is None:
                print(f"{Fore.RED}读取测试用例失败{Style.RESET_ALL}")
                return

            self.setup_driver()

            total_cases = len(test_cases)
            success_cases = 0
            failed_cases = 0

            print(f"\n{Fore.CYAN}开始执行测试套件...{Style.RESET_ALL}")
            print(f"{Fore.CYAN}总用例数: {total_cases}{Style.RESET_ALL}\n")

            for index, test_case in test_cases.iterrows():
                print(f"\n{Fore.CYAN}执行测试用例: {test_case['测试用例编号']}{Style.RESET_ALL}")
                print(f"{Fore.CYAN}测试点: {test_case['测试点']}{Style.RESET_ALL}\n")

                result = self.execute_step(test_case)
                if result:
                    success_cases += 1
                else:
                    failed_cases += 1
                    logging.error(f"测试用例 {test_case['测试用例编号']} 执行失败")

            print(f"\n{Fore.CYAN}测试执行完成{Style.RESET_ALL}")
            print(f"{Fore.GREEN}成功用例数: {success_cases}{Style.RESET_ALL}")
            print(f"{Fore.RED}失败用例数: {failed_cases}{Style.RESET_ALL}")
            print(f"{Fore.CYAN}总用例数: {total_cases}{Style.RESET_ALL}")
            time.sleep(1)

            # 生成测试报告
            report_path = self.generate_html_report()
            print(f"\n{Fore.CYAN}测试报告生成在: {report_path}{Style.RESET_ALL}")

            # 自动打开测试报告
            import webbrowser
            webbrowser.open(f'file://{os.path.abspath(report_path)}')

        except Exception as e:
            print(f"{Fore.RED}生成测试报告时发生错误: {str(e)}{Style.RESET_ALL}")
            logging.error(f"生成测试报告失败: {str(e)}")


if __name__ == "__main__":
    test = AndroidUITest()
    test.run_test_suite("C:\\Users\\leixin\\Desktop\\APP_test.xlsx")
